/*
 * Unidata Platform
 * Copyright (c) 2013-2020, UNIDATA LLC, All rights reserved.
 *
 * Commercial License
 * This version of Unidata Platform is licensed commercially and is the appropriate option for the vast majority of use cases.
 *
 * Please see the Unidata Licensing page at: https://unidata-platform.com/license/
 * For clarification or additional options, please contact: info@unidata-platform.com
 * -------
 * Disclaimer:
 * -------
 * THIS SOFTWARE IS DISTRIBUTED "AS-IS" WITHOUT ANY WARRANTIES, CONDITIONS AND
 * REPRESENTATIONS WHETHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION THE
 * IMPLIED WARRANTIES AND CONDITIONS OF MERCHANTABILITY, MERCHANTABLE QUALITY,
 * FITNESS FOR A PARTICULAR PURPOSE, DURABILITY, NON-INFRINGEMENT, PERFORMANCE AND
 * THOSE ARISING BY STATUTE OR FROM CUSTOM OR USAGE OF TRADE OR COURSE OF DEALING.
 */
package org.unidata.mdm.dq.core.service.impl.function.system.string;

import static org.unidata.mdm.dq.core.type.constant.CleanseConstants.INPUT_PORT_1;
import static org.unidata.mdm.dq.core.type.constant.CleanseConstants.INPUT_PORT_2;
import static org.unidata.mdm.dq.core.type.constant.CleanseConstants.OUTPUT_PORT_1;

import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.regex.Pattern;

import org.apache.commons.lang3.StringUtils;
import org.unidata.mdm.core.type.data.Attribute;
import org.unidata.mdm.dq.core.context.CleanseFunctionContext;
import org.unidata.mdm.dq.core.dto.CleanseFunctionResult;
import org.unidata.mdm.dq.core.service.impl.function.system.AbstractSystemCleanseFunction;
import org.unidata.mdm.dq.core.type.cleanse.CleanseFunctionConfiguration;
import org.unidata.mdm.dq.core.type.cleanse.CleanseFunctionExecutionScope;
import org.unidata.mdm.dq.core.type.cleanse.CleanseFunctionInputParam;
import org.unidata.mdm.dq.core.type.cleanse.CleanseFunctionOutputParam;
import org.unidata.mdm.dq.core.type.cleanse.CleanseFunctionPortFilteringMode;
import org.unidata.mdm.dq.core.type.cleanse.CleanseFunctionPortInputType;
import org.unidata.mdm.dq.core.type.cleanse.CleanseFunctionPortValueType;
import org.unidata.mdm.dq.core.type.constant.CleanseConstants;
import org.unidata.mdm.system.util.TextUtils;

import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;

/**
 * Checks string value by mask.
 *
 * @author ilya.bykov
 */
public class CheckMask extends AbstractSystemCleanseFunction {
    /**
     * Display name code.
     */
    private static final String FUNCTION_DISPLAY_NAME = "app.dq.functions.system.string.check.mask.display.name";
    /**
     * Description code.
     */
    private static final String FUNCTION_DESCRIPTION = "app.dq.functions.system.string.check.mask.decsription";
    /**
     * IP1 name code.
     */
    private static final String INPUT_PORT_1_NAME = "app.dq.functions.system.string.check.mask.input.port1.name";
    /**
     * IP1 description code.
     */
    private static final String INPUT_PORT_1_DESCRIPTION = "app.dq.functions.system.string.check.mask.input.port1.decsription";
    /**
     * IP2 name code.
     */
    private static final String INPUT_PORT_2_NAME = "app.dq.functions.system.string.check.mask.input.port2.name";
    /**
     * IP2 description code.
     */
    private static final String INPUT_PORT_2_DESCRIPTION = "app.dq.functions.system.string.check.mask.input.port2.decsription";
    /**
     * IP3 name code.
     */
    private static final String INPUT_PORT_3_NAME = "app.dq.functions.system.string.check.mask.input.port3.name";
    /**
     * IP3 description code.
     */
    private static final String INPUT_PORT_3_DESCRIPTION = "app.dq.functions.system.string.check.mask.input.port3.decsription";
    /**
     * IP4 name code.
     */
    private static final String INPUT_PORT_4_NAME = "app.dq.functions.system.string.check.mask.input.port4.name";
    /**
     * IP4 description code.
     */
    private static final String INPUT_PORT_4_DESCRIPTION = "app.dq.functions.system.string.check.mask.input.port4.decsription";
    /**
     * OP1 name code.
     */
    private static final String OUTPUT_PORT_1_NAME = "app.dq.functions.system.string.check.mask.output.port1.name";
    /**
     * OP1 description code.
     */
    private static final String OUTPUT_PORT_1_DESCRIPTION = "app.dq.functions.system.string.check.mask.output.port1.decsription";
    /**
     * OP2 name code.
     */
    private static final String OUTPUT_PORT_2_NAME = "app.dq.functions.system.string.check.mask.output.port2.name";
    /**
     * OP2 description code.
     */
    private static final String OUTPUT_PORT_2_DESCRIPTION = "app.dq.functions.system.string.check.mask.output.port2.decsription";
    /**
     * This function configuration.
     */
    private static final CleanseFunctionConfiguration CONFIGURATION
        = CleanseFunctionConfiguration.configuration()
            .supports(CleanseFunctionExecutionScope.GLOBAL, CleanseFunctionExecutionScope.LOCAL)
            .input(CleanseFunctionConfiguration.port()
                    .name(INPUT_PORT_1)
                    .displayName(() -> TextUtils.getText(INPUT_PORT_1_NAME))
                    .description(() -> TextUtils.getText(INPUT_PORT_1_DESCRIPTION))
                    .filteringMode(CleanseFunctionPortFilteringMode.MODE_ALL)
                    .inputTypes(CleanseFunctionPortInputType.SIMPLE)
                    .valueTypes(CleanseFunctionPortValueType.STRING)
                    .required(false)
                    .build())
            .input(CleanseFunctionConfiguration.port()
                    .name(INPUT_PORT_2)
                    .displayName(() -> TextUtils.getText(INPUT_PORT_2_NAME))
                    .description(() -> TextUtils.getText(INPUT_PORT_2_DESCRIPTION))
                    .filteringMode(CleanseFunctionPortFilteringMode.MODE_ALL)
                    .inputTypes(CleanseFunctionPortInputType.SIMPLE)
                    .valueTypes(CleanseFunctionPortValueType.STRING)
                    .required(false)
                    .build())
            .input(CleanseFunctionConfiguration.port()
                    .name(CleanseConstants.INPUT_PORT_3)
                    .displayName(() -> TextUtils.getText(INPUT_PORT_3_NAME))
                    .description(() -> TextUtils.getText(INPUT_PORT_3_DESCRIPTION))
                    .filteringMode(CleanseFunctionPortFilteringMode.MODE_ALL)
                    .inputTypes(CleanseFunctionPortInputType.SIMPLE, CleanseFunctionPortInputType.ARRAY, CleanseFunctionPortInputType.CODE)
                    .valueTypes(CleanseFunctionPortValueType.STRING)
                    .required(false)
                    .build())
            .input(CleanseFunctionConfiguration.port()
                    .name(CleanseConstants.INPUT_PORT_4)
                    .displayName(() -> TextUtils.getText(INPUT_PORT_4_NAME))
                    .description(() -> TextUtils.getText(INPUT_PORT_4_DESCRIPTION))
                    .filteringMode(CleanseFunctionPortFilteringMode.MODE_ALL)
                    .inputTypes(CleanseFunctionPortInputType.SIMPLE)
                    .valueTypes(CleanseFunctionPortValueType.BOOLEAN)
                    .required(false)
                    .build())
            .output(CleanseFunctionConfiguration.port()
                    .name(OUTPUT_PORT_1)
                    .displayName(() -> TextUtils.getText(OUTPUT_PORT_1_NAME))
                    .description(() -> TextUtils.getText(OUTPUT_PORT_1_DESCRIPTION))
                    .filteringMode(CleanseFunctionPortFilteringMode.MODE_ALL)
                    .inputTypes(CleanseFunctionPortInputType.SIMPLE)
                    .valueTypes(CleanseFunctionPortValueType.BOOLEAN)
                    .required(true)
                    .build())
            .output(CleanseFunctionConfiguration.port()
                    .name(CleanseConstants.OUTPUT_PORT_2)
                    .displayName(() -> TextUtils.getText(OUTPUT_PORT_2_NAME))
                    .description(() -> TextUtils.getText(OUTPUT_PORT_2_DESCRIPTION))
                    .filteringMode(CleanseFunctionPortFilteringMode.MODE_ALL)
                    .inputTypes(CleanseFunctionPortInputType.SIMPLE)
                    .valueTypes(CleanseFunctionPortValueType.STRING)
                    .required(true)
                    .build())
            .build();
    /**
     * The regexp cache.
     */
    private LoadingCache<String, Pattern> regexpCache = CacheBuilder.newBuilder()
            .expireAfterWrite(60, TimeUnit.MINUTES)
            .build(new CacheLoader<String, Pattern>() {

                @Override
                public Pattern load(String regexp) {
                    return Pattern.compile(regexp);

                }
            });
    /**
     * Instantiates a new CF check mask.
     */
    public CheckMask() {
        super("CheckMask", () -> TextUtils.getText(FUNCTION_DISPLAY_NAME), () -> TextUtils.getText(FUNCTION_DESCRIPTION));
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public CleanseFunctionConfiguration configure() {
        return CONFIGURATION;
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public CleanseFunctionResult execute(CleanseFunctionContext ctx) {

        CleanseFunctionResult output = new CleanseFunctionResult();

        // Regex, mask, value, required
        CleanseFunctionInputParam param1 = ctx.getInputParam(CleanseConstants.INPUT_PORT_1);
        CleanseFunctionInputParam param2 = ctx.getInputParam(CleanseConstants.INPUT_PORT_2);
        CleanseFunctionInputParam param3 = ctx.getInputParam(CleanseConstants.INPUT_PORT_3);
        CleanseFunctionInputParam param4 = ctx.getInputParam(CleanseConstants.INPUT_PORT_4);

        String regexp = param1 != null && !param1.isEmpty() ? param1.toSingletonValue() : null;
        String mask =  param2 != null && !param2.isEmpty() ? param2.toSingletonValue() : null;
        boolean required = param4 != null && !param4.isEmpty() && param4.<Boolean>toSingletonValue();
        boolean noValidationParams = StringUtils.isAllBlank(mask, regexp);
        boolean noValidationValue = param3.isEmpty();

        if (noValidationParams || noValidationValue) {

            Boolean result;
            if (required) {
                result = noValidationParams ? Boolean.TRUE : Boolean.FALSE;
            } else {
                result = Boolean.TRUE;
            }

            output.putOutputParam(CleanseFunctionOutputParam.of(CleanseConstants.OUTPUT_PORT_1, result));
            output.putOutputParam(CleanseFunctionOutputParam.of(CleanseConstants.OUTPUT_PORT_2, StringUtils.EMPTY));
        } else {

            regexp = StringUtils.isBlank(regexp) ? RegexpUtils.convertMaskToRegexString(mask) : regexp;

            boolean hasFailed = false;
            Map<Object, List<Attribute>> values = extractAndMapAttributes(param3);
            for (Iterator<Entry<Object, List<Attribute>>> ei = values.entrySet().iterator(); ei.hasNext(); ) {

                Entry<Object, List<Attribute>> entry = ei.next();
                if (StringUtils.isBlank(entry.getKey().toString()) && !required) {
                    ei.remove();
                    continue;
                }

                try {

                    if (!RegexpUtils.validate(regexpCache.get(regexp), entry.getKey().toString())) {
                        hasFailed = true;
                        entry.getValue().forEach(attr -> output.addSpot(attr.toLocalPath(), attr));
                    } else {
                        ei.remove();
                    }

                } catch (ExecutionException e) {
                    hasFailed = true;
                    entry.getValue().forEach(attr -> output.addSpot(attr.toLocalPath(), attr));
                    ei.remove();
                }
            }

            output.putOutputParam(CleanseFunctionOutputParam.of(CleanseConstants.OUTPUT_PORT_1, !hasFailed));
            output.putOutputParam(CleanseFunctionOutputParam.of(CleanseConstants.OUTPUT_PORT_2, !hasFailed
                    ? StringUtils.EMPTY
                    :TextUtils.getText("app.dq.cleanse.validation.mask",
                            values.keySet().toString(),
                            output.getSpots().toString(),
                            mask)));
        }

        return output;
    }
}
